﻿#define MONO_CECIL

#if MONO_CECIL
#else
#define _NET_REFLECTOR
#endif

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Model;

namespace XmlDBAdapter
{
    public class Adapter
    {
        #region Lifecycle

        public Adapter() { }

        #endregion Lifecycle

        #region Properties

        private ActualEntities pContext;
        public ActualEntities Context
        {
            get
            {
                return pContext;
            }
            set
            {
                if (value != pContext)
                    pContext = value;
            }
        }

        #endregion Properties

        #region Methods

#if DEBUG
        static int sFirstTry = 0;
        static int sSecondTry = 0;
        static int sThirdTry = 0;
        static int sFourthTry = 0;
        static int sFifthTry = 0;
        static int sJustFirstTry = 0;
        static int sJustSecondTry = 0;
        static int sJustThirdTry = 0;
        static int sJustFourthTry = 0;
        static int sJustFifthTry = 0;
#endif

        /// <summary>
        /// Parses the XML content of the given FxCop 10.0 report and stores it to the database.
        /// </summary>
        /// <param name="analysisReportPath">Describes the filename and path of the file that contains
        /// the FxCop report</param>
        /// <exception cref="ArgumentNullException">
        /// When <para name="analysisReportPath"/> is null
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// When <para name="context"/> is null, or not set and <see cref="Context"/> is null
        /// </exception>
        /// <exception cref="FileNotFoundException">
        /// When the file with the given path and name described in <para name="analysisReportPath"/>
        /// is not exists
        /// </exception>
        public void ParseToDB(string analysisReportPath, ActualEntities context = null)
        {
            #region ...
            if (analysisReportPath == null)
                throw new ArgumentNullException("analysisReportPath");
            if (File.Exists(analysisReportPath) == false)
                throw new FileNotFoundException("File not found", analysisReportPath);
            if (context == null && this.Context == null)
                throw new ArgumentNullException("context", "If context is not set, Context must be set!");

            if (context != null && this.Context != context)
                this.Context = context;

            List<string> namespaceNames = new List<string>(this.Context.Namespaces.Count());
            foreach (var ns in this.Context.Namespaces)
                namespaceNames.Add(ns.Name);

            XElement root = XElement.Load(analysisReportPath, LoadOptions.SetLineInfo);
            var targets = from element in root.Element("Targets").Elements("Target")
                          select element;
            // Assemblies
            foreach (var target in targets)
            {
                System.Reflection.Assembly targettedAssembly =
                    System.Reflection.Assembly.ReflectionOnlyLoadFrom(target.Attribute("Name").Value);
                string assemblyName = targettedAssembly.FullName.Split(',').First();
#if _NET_REFLECTOR
                Assembly assembly =
                    this.Context.Assemblies.Where(a => a.Name.Equals(assemblyName)).First();
#elif MONO_CECIL
                Assembly assembly =
                    this.Context.Assemblies.Where(a => a.Name.StartsWith(assemblyName + ",")).First();
#endif
                var modules = from element in target.Element("Modules").Elements("Module")
                              select element;
                bool storeModuleMessagesOnAssembly = (modules.Count() == 1);
                // Modules
                foreach (var mod in modules)
                {
                    string moduleName = target.Attribute("Name").Value.Split('\\').Last();//mod.Attribute("Name").Value;
                    Module module = this.Context.Modules.Where(
                        m => m.AssemblyName.Equals(assembly.Name)
                            && m.Name.Equals(moduleName)).First();
                    // Messages
                    {
                        var msgs = mod.Element("Messages");
                        if (msgs != null)
                        {
                            var messages = from element in msgs.Elements("Message")
                                           select element;
                            ParseMessage(messages, assembly, module,
                                storeModuleMessagesOnAssembly: storeModuleMessagesOnAssembly);
                        }
                    }
                    // Namespaces
                    var namespaces = from element in mod.Element("Namespaces").Elements("Namespace")
                                     select element;
                    foreach (var n in namespaces)
                    {
                        string namespaceName = n.Attribute("Name").Value;
                        Namespace ns = this.Context.Namespaces.Where(
                        p => p.Name.Equals(namespaceName)).First();
                        // Messages
                        {
                            var msgs = n.Element("Messages");
                            if (msgs != null)
                            {
                                var messages = from element in msgs.Elements("Message")
                                               select element;
                                ParseMessage(messages, assembly, module, ns,
                                    storeModuleMessagesOnAssembly: storeModuleMessagesOnAssembly);
                            }
                        }
                        // Types
                        var types = from element in n.Element("Types").Elements("Type")
                                    select element;
                        foreach (var t in types)
                        {
                            //TODO_REVIEW: kell spec char.kód feloldás?
                            string typeName = ResolveSpecialCharacters(/*namespaceName + "." + */t.Attribute("Name").Value);
#if _NET_REFLECTOR
                            Model.Type type = this.Context.Types.Where(
                                p => p.Name.Equals(typeName) && p.NamespaceName.Equals(ns.Name)
                                    && p.ModuleName.Equals(module.Name)
                                    && p.ModuleAssemblyName.Equals(assembly.Name)).First();
#elif MONO_CECIL
                            Model.Type type = this.Context.Types.Where(
                                p => p.Name.Equals(typeName) && p.NamespaceName.Equals(ns.Name)
                                    && p.ModuleName.Equals(module.Name)
                                    && p.ModuleAssemblyName.Equals(assembly.Name)).First();
#endif
                            // Messages
                            {
                                var msgs = t.Element("Messages");
                                if (msgs != null)
                                {
                                    var messages = from element in msgs.Elements("Message")
                                                   select element;
                                    ParseMessage(messages, assembly, module, ns, type,
                                        storeModuleMessagesOnAssembly: storeModuleMessagesOnAssembly);
                                }
                            }
            #endregion ...
                            if (t.Element("Members") != null)
                            {
                                // Members
                                var members = from element in t.Element("Members").Elements("Member")
                                              select element;
                                foreach (var m in members)
                                {
                                    ParseMember(namespaceNames, assembly, storeModuleMessagesOnAssembly,
                                        module, ns, typeName, type, m);
                                } // each member
                            }
                        } // each type
                    } // each namespace
                } // each module
            } // each target
#if DEBUG
            Debug.WriteLine(
                "[Adapter] 1st: " + sJustFirstTry.ToString() + "/" + sFirstTry.ToString()
                + sJustSecondTry.ToString() + "/" + " 2nd: " + sSecondTry.ToString()
                + sJustThirdTry.ToString() + "/" + " 3rd: " + sThirdTry.ToString()
                + sJustFourthTry.ToString() + "/" + " 4th: " + sFourthTry.ToString()
                + sJustFifthTry.ToString() + "/" + " 5th: " + sFifthTry.ToString());
#endif
        }

        delegate bool ComparerDelegate(Member m, string s);
        /// <summary>
        /// 
        /// </summary>
        /// <param name="namespaceNames">List of all used namespaces</param>
        /// <param name="assembly">Containing assembly</param>
        /// <param name="storeModuleMessagesOnAssembly">Determines wether the FxCop messages on a member
        /// will be stored also as message on the containing assembly, or not</param>
        /// <param name="module">Containing module</param>
        /// <param name="ns">Containing namespace</param>
        /// <param name="preprocessedTypeName">Preprocessed typename from FxCop output</param>
        /// <param name="type">Containing type</param>
        /// <param name="memberX">Part of the FxCop output about the member</param>
        private void ParseMember(List<string> namespaceNames, Assembly assembly,
            bool storeModuleMessagesOnAssembly, Module module, Namespace ns, string preprocessedTypeName,
            Model.Type type, XElement memberX)
        {
            string pureMemberName =
                ResolveSpecialCharacters(memberX.Attribute("Name").Value.TrimStart('#'));
            string resolvedGetterIndexer, resolvedSetterIndexer;
            string[] memberNames;
            if (TryResolveIndexer(pureMemberName,
                out resolvedGetterIndexer, out resolvedSetterIndexer) == true)
                memberNames = new string[2] { resolvedGetterIndexer, resolvedSetterIndexer };
            else
                memberNames = new string[1] { pureMemberName };

            bool foundMember = false;
            int i = 0;
            foreach (var memberN in memberNames)
            {
                string memberName = preprocessedTypeName + "." + memberN;
                string memberNameWithoutNamespaces =
                    EliminateNamespaces(memberName, namespaceNames);
                string memberNameWithoutNamespacesWithSpaces =
                        memberNameWithoutNamespaces.Replace(",", ", ");
                string pureMemberNameWithSpaces = memberN.Replace(",", ", ");

                IQueryable<Member> query = null;
#if DEBUG
                bool justFirstTry = true, justSecondTry = true, justThirdTry = true,
                    justFourthTry = true, justFifthTry = true;
                sFirstTry++;
#endif
                Member member = null;
                if (memberN.Contains('!') == false)
                {
                    memberName = ResolveMemberOfGeneric(memberName);
                    query = this.Context.Members.Where(
                        p => p.Name.StartsWith(memberName) && p.TypeName.Equals(type.Name)
                            && p.TypeNamespaceName.Equals(ns.Name)
                            && p.TypeModuleName.Equals(module.Name)
                            && p.TypeModuleAssemblyName.Equals(assembly.Name));
                    if (query.Count() == 0)
                    {
                        Debug.WriteLine("[Adapter] " + memberName + " not found");
                        memberNameWithoutNamespaces = ResolveMemberOfGeneric(memberNameWithoutNamespaces);
                        query = this.Context.Members.Where(
                            p => p.Name.StartsWith(memberNameWithoutNamespaces)
                                && p.TypeName.Equals(type.Name)
                                && p.TypeNamespaceName.Equals(ns.Name)
                                && p.TypeModuleName.Equals(module.Name)
                                && p.TypeModuleAssemblyName.Equals(assembly.Name));
#if DEBUG
                        sSecondTry++;
                        justFirstTry = false;
#endif
                    }
                    if (query.Count() == 0)
                    {
                        Debug.WriteLine("[Adapter] " + memberNameWithoutNamespaces + " not found");
                        memberNameWithoutNamespacesWithSpaces =
                            ResolveMemberOfGeneric(memberNameWithoutNamespacesWithSpaces);
                        query = this.Context.Members.Where(
                            p => p.Name.StartsWith(memberNameWithoutNamespacesWithSpaces)
                                && p.TypeName.Equals(type.Name)
                                && p.TypeNamespaceName.Equals(ns.Name)
                                && p.TypeModuleName.Equals(module.Name)
                                && p.TypeModuleAssemblyName.Equals(assembly.Name));
#if DEBUG
                        sThirdTry++;
                        justSecondTry = false;
#endif
                    }
                    if (query.Count() == 0) // case of properties (not get_pr or set_pr, just pr)
                    {
                        Debug.WriteLine("[Adapter] " + memberNameWithoutNamespacesWithSpaces + " not found");
                        string resolvedMemberN = ResolveMemberOfGeneric(memberN);
                        query = this.Context.Members.Where(
                            p => p.Name.StartsWith(resolvedMemberN)
                                && p.TypeName.Equals(type.Name)
                                && p.TypeNamespaceName.Equals(ns.Name)
                                && p.TypeModuleName.Equals(module.Name)
                                && p.TypeModuleAssemblyName.Equals(assembly.Name));
#if DEBUG
                        sFourthTry++;
                        justThirdTry = false;
#endif
                    }
                    if (query.Count() == 0)
                    {
                        Debug.WriteLine("[Adapter] " + memberN + " not found");
                        pureMemberNameWithSpaces = ResolveMemberOfGeneric(pureMemberNameWithSpaces);
                        query = this.Context.Members.Where(
                            p => p.Name.StartsWith(pureMemberNameWithSpaces)
                                && p.TypeName.Equals(type.Name)
                                && p.TypeNamespaceName.Equals(ns.Name)
                                && p.TypeModuleName.Equals(module.Name)
                                && p.TypeModuleAssemblyName.Equals(assembly.Name));
#if DEBUG
                        sFifthTry++;
#endif
                    }
                    try
                    {
                        member = query.First();
                    }
                    catch (InvalidOperationException) { }
                    catch (NullReferenceException) { }
                }
                else
                {
                    //DB: member:    GenericType<T,S>..ctor(T, S) : Void
                    //    type:      GenericType`2
                    //FxCop: member: #.ctor(!0,!1)
                    //       type:   GenericType`2
                    //
                    //ComparerDelegate cd = new ComparerDelegate(AreTheSame);
                    query = this.Context.Members.Where(
                        p => p.TypeName.Equals(type.Name));
                    foreach (var item in query)
                    {
                        if (AreTheSame(item, memberNameWithoutNamespaces) == true)
                        {
                            member = item;
                            break;
                        }
                    }
                }
                //Debug.Assert(query.Count() > 0);

                if (member != null)
                {
                    // Messages
                    {
                        var msgs = memberX.Element("Messages");
                        if (msgs != null)
                        {
                            var messages = from element in msgs.Elements("Message")
                                           select element;
                            ParseMessage(messages, assembly, module, ns, type, member,
                                storeModuleMessagesOnAssembly: storeModuleMessagesOnAssembly);
                        }
                    }
                    foundMember = true;
                }
#if DEBUG
                i++;
                Debug.Assert(
                    (i == memberNames.Length && foundMember == false) == false);
                if (justFirstTry == true)
                    sJustFirstTry++;
                if (justSecondTry == true)
                    sJustSecondTry++;
                if (justThirdTry == true)
                    sJustThirdTry++;
                if (justFourthTry == true)
                    sJustFourthTry++;
                if (justFifthTry == true)
                    sJustFifthTry++;
#endif
            } // each memberNames
            if (foundMember == false)
                Trace.TraceError(memberX.ToString() + " not found!");
        }

        private static string ResolveMemberOfGeneric(string memberName)
        {
#if _NET_REFLECTOR
            if (memberName.Contains('`') == false)
                return memberName;

            for (int i = 0; i < 10; i++)
                memberName = memberName.Replace("`" + i.ToString(), "");
            return memberName;
#elif MONO_CECIL
            return memberName;
#endif
        }

#if _NET_REFLECTOR
        private bool AreTheSame(Member p, string memberNameWithoutNamespaces)
        {
            //DB: member:    System.Void AnalysisTestTarget.GenericType`2::.ctor(T,S)   MOMO_CECIL
            //DB: member:    GenericType<T,S>..ctor(T, S) : Void                        _NET_REFLECTOR
            //FxCop: member: GenericType`2..ctor(!0,!1)
            string dbMemberName = p.Name.Replace(" ", "");
            string[] dbMemberNameTokens = dbMemberName.Split(new char[] { ',', '.', '(', ')' },
                StringSplitOptions.RemoveEmptyEntries);
            string[] fxCopMemberNameTokens =
                memberNameWithoutNamespaces.Split(new char[] { ',', '.', '(', ')' },
                    StringSplitOptions.RemoveEmptyEntries);
            //GenericType<T  S>  ctor  T   S   :Void
            //GenericType`2      ctor  !0  !1
            int numberOfGenericParams = int.Parse(fxCopMemberNameTokens[0].Split(new char[] { '`' })[1]);
            int offset = numberOfGenericParams - 1;
            if (Math.Abs(dbMemberNameTokens.Length - fxCopMemberNameTokens.Length) != 2)
                return false;
            int lessLengt = dbMemberNameTokens.Length < fxCopMemberNameTokens.Length
                ? dbMemberNameTokens.Length
                : fxCopMemberNameTokens.Length;
            for (int i = 1 + offset; i < lessLengt; i++)
            {
                string fxCopToken = fxCopMemberNameTokens[i - offset];
                string dbToken = dbMemberNameTokens[i];
                if ((fxCopToken.Equals(dbToken) == true
                    || (fxCopToken.Equals(dbToken) == false
                        && fxCopToken.StartsWith("!") == true)) == false)
                    return false;
            }
            return true;
        }
#elif MONO_CECIL
        private bool AreTheSame(Member p, string memberNameWithoutNamespaces)
        {
            //DB: member:    AnalysisTestTarget.GenericType`2..ctor(T,S)
            //FxCop: member: GenericType`2..ctor(!0,!1)

            string dbMemberName = EliminateNamespaces(p.Name.Replace(" ", ""),
                this.Context.Namespaces.Select(ns => ns.Name));
            //GenericType`2..ctor(T,S)
            string[] dbMemberNameTokens = dbMemberName.Split(new char[] { ',', '.', '(', ')' },
                StringSplitOptions.RemoveEmptyEntries);
            string[] fxCopMemberNameTokens =
                memberNameWithoutNamespaces.Split(new char[] { ',', '.', '(', ')' },
                    StringSplitOptions.RemoveEmptyEntries);

            //GenericType`2      ctor  T   S
            //GenericType`2      ctor  !0  !1
            if (Math.Abs(dbMemberNameTokens.Length - fxCopMemberNameTokens.Length) != 0)
                return false;
            bool hasSameNotation = true;
            for (int i = 1; i < fxCopMemberNameTokens.Length; i++)
            {
                string fxCopToken = fxCopMemberNameTokens[i];
                string dbToken = dbMemberNameTokens[i];
                if (fxCopToken.Equals(dbToken) == false)
                {
                    hasSameNotation = false;
                    break;
                }
            }

            if (hasSameNotation == true)
                return true;

            Dictionary<string, string> equivalence = new Dictionary<string, string>();
            for (int i = 2; i < fxCopMemberNameTokens.Length; i++)
            {
                string fxCopToken = fxCopMemberNameTokens[i];
                string dbToken = dbMemberNameTokens[i];
                if (fxCopToken.StartsWith("!") == true)
                {
                    if (equivalence.Keys.Contains(fxCopToken) == true
                        && equivalence[fxCopToken].Equals(dbToken) == false)
                        return false;
                    else
                        equivalence.Add(fxCopToken, dbToken);

                }
                else
                    return false;
            }

            return true;
        }
#endif

        private string ResolveSpecialCharacters(string name)
        {
            string resolvedName = name.Replace("&lt;", "<");
            resolvedName = resolvedName.Replace("&lt;", "<");
            resolvedName = resolvedName.Replace("&gt;", ">");
            //for (int i = 0; i < 10; i++)
            //    resolvedName = resolvedName.Replace("`" + i.ToString(), "");
            // `i azt jelöli, hogy hány típusparamétere van a generikus típusnak
            return resolvedName;
        }

        private bool TryResolveIndexer(string pureMemberName,
            out string resolvedGetterIndexer, out string resolvedSetterIndexer)
        {
            if (pureMemberName.Contains("[]") == false && pureMemberName.Contains('[') == true
                && pureMemberName.Contains(']') == true)
            {
                string indexer = pureMemberName.Replace('[', '(').Replace(']', ')');
                resolvedGetterIndexer = "get_" + indexer;
                resolvedSetterIndexer = "set_" + indexer;
                return true;
            }
            else
            {
                resolvedGetterIndexer = null;
                resolvedSetterIndexer = null;
                return false;
            }
        }

        private static string EliminateNamespaces(string memberName, IEnumerable<string> namespaces)
        {
            string[] tokens = memberName.Replace(" ", "")
                .Split(new char[] { '(' }, StringSplitOptions.RemoveEmptyEntries);
            if (tokens.Length == 2)
            {
                string[] tokens1 =
                    tokens[1].Split(new char[] { ')' }, StringSplitOptions.RemoveEmptyEntries);
                string parameters = "";
                if (tokens1.Length > 0)
                {
                    /* a.b.t1,a.t2,c.t3 -> .a..b..t1.,.a..t2,.c..t3.
                     * a.g<a.b.t>,a.t2  -> .a..g<.a..b..t.>.,.a..t2.
                     */
                    if (tokens1[0].Contains('.') == false)
                        parameters = tokens1[0];
                    else
                    {
                        parameters =
                            "."
                            + (tokens1[0].Replace(".", "..").Replace(",", ".,.")
                                .Replace("<", "<.").Replace(">", ".>"))
                            + ".";
                        foreach (var ns in namespaces)//this.Context.Namespaces)
                        {
                            string[] nsTokens = ns./*Name.*/Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                            foreach (var token in nsTokens)
                            {
                                parameters = parameters.Replace("." + token + ".", "");
                            }
                        }
                        /* .a..b..t1.,.a..t2,.c..t3  -> .t1.,.t2.,.t3.
                         * .a..g<.a..b..t.>.,.a..t2. -> .g<.t.>.,.t2.
                         */
                        Debug.Assert(parameters.Count(ch => ch.Equals('.'))
                            == parameters.Count(ch => ch.Equals(',')) * 2 + 2
                            + parameters.Count(ch => ch.Equals('<')) + parameters.Count(ch => ch.Equals('>')));
                        /* .t1.,.t2.,.t3. -> t1,t2,t3
                         * .g<.t.>.,.t2.  -> g<t>,t2
                         */
                        parameters = parameters.Replace(".", "");
                        Debug.Assert(parameters.Count(ch => ch.Equals('.')) == 0, "Not all namespace prefixes are elliminated!");
                    }
                }
                memberName = tokens[0] + "(" + parameters + ")" + (tokens1.Length > 1 ? tokens1[1] : "");
                memberName.Replace(",", ", ");
            }
            return memberName;
        }

        private void ParseMessage(IEnumerable<XElement> messages, Assembly assembly, Module module,
            Namespace ns = null, Model.Type type = null, Member member = null,
            bool storeModuleMessagesOnAssembly = false)
        {
            foreach (var mess in messages)
            {
                Message_FxCopMessage message =
                    new Message_FxCopMessage()
                    {
                        Created = DateTime.Parse(mess.Attribute("Created").Value),
                        FixCategory = mess.Attribute("FixCategory").Value,
                        Status = mess.Attribute("Status").Value
                    };
                string checkId = mess.Attribute("CheckId").Value;
                Rule_FxCopRule fxCopRule = this.Context.Rule_FxCopRule.Where(
                    f => f.Id.Equals(checkId)).First();
                Message baseMessage =
                    this.Context.Message_FxCopMessage.AddObjectSafe(entity: message, rule: fxCopRule.Rule,
                        assembly: assembly, module: module, ns: ns, type: type, member: member);
                #region Issues
                var issues = from element in mess.Elements("Issue")
                             select element;
                foreach (var i in issues)
                {
                    XAttribute file = i.Attribute("File");
                    XAttribute line = i.Attribute("Line");
                    XAttribute name = i.Attribute("Name");
                    XAttribute path = i.Attribute("Path");
                    FxCopIssue issue =
                        new FxCopIssue()
                        {
                            Certainty = int.Parse(i.Attribute("Certainty").Value),
                            File = (file == null ? null : file.Value),
                            //FxCopMessageId = message.Id,
                            Level = i.Attribute("Level").Value,
                            Line = (line == null ? null : line.Value),
                            Message_FxCopMessage = message,
                            Name = (name == null ? null : name.Value),
                            Path = (path == null ? null : path.Value),
                            Content = (string)i
                        };
                    //this.Context.FxCopIssues.AddObject(issue);
                    message.FxCopIssues.Add(issue);
                    this.Context.SaveChanges();
                }
                #endregion Issues
                baseMessage.Assembly = assembly;
                baseMessage.AssemblyName = assembly.Name;
                if (storeModuleMessagesOnAssembly == true)
                    assembly.Messages.Add(baseMessage);
                baseMessage.ModuleAssemblyName = assembly.Name;
                //baseMessage.Module = module;
                baseMessage.ModuleName = module.Name;
                //module.Messages.Add(baseMessage);
                if (ns != null)
                {
                    baseMessage.Namespace = ns;
                    baseMessage.NamespaceName = ns.Name;
                    ns.Messages.Add(baseMessage);
                }
                if (type != null)
                {
                    baseMessage.Type = type;
                    baseMessage.TypeName = type.Name;
                    baseMessage.TypeNamespaceName = type.NamespaceName;
                    baseMessage.TypeModuleName = type.ModuleName;
                    baseMessage.TypeModuleAssemblyName = type.ModuleAssemblyName;
                    type.Messages.Add(baseMessage);
                }
                if (member != null)
                {
                    baseMessage.Member = member;
                    baseMessage.MemberName = member.Name;
                    baseMessage.MemberTypeName = member.TypeName;
                    baseMessage.MemberTypeNamespaceName = member.TypeNamespaceName;
                    baseMessage.MemberTypeModuleName = member.TypeModuleName;
                    baseMessage.MemberTypeModuleAssemblyName = member.TypeModuleAssemblyName;
                    member.Messages.Add(baseMessage);
                }
                this.Context.SaveChanges();
            }
        }

        #endregion Methods
    }
}
